/// This module is for generating schema in SDL (Schema Definition Language) format.
/// An example of schema in SDL format is:
/// ```graphql
/// schema {
///   query: Query
///   mutation: Mutation
///   subscription: Subscription
/// }
///
/// type Query {
///   allPersons(last: Int): [Person!]!
///   allPosts(last: Int): [Post!]!
/// }
///
/// type Mutation {
///   createPerson(name: String!, age: Int!): Person!
///   updatePerson(id: ID!, name: String!, age: Int!): Person!
///   deletePerson(id: ID!): Person!
/// }
///
/// type Subscription {
///   newPerson: Person!
/// }
///
/// type Person {
///   id: ID!
///   name: String!
///   age: Int!
///   posts: [Post!]!
/// }
///
/// type Post {
///   title: String!
///   author: Person!
/// }
/// ```
///
/// The schema can generated by calling `generate_sdl` method on the `Schema` struct.
///
use std::collections::BTreeMap;

use crate::{
    ast::{common as ast, value::ConstValue},
    schema::{
        DeprecationStatus, Directive, Enum, Field, InputField, InputObject, Interface, Namespaced,
        NamespacedGetter, Object, Scalar, Schema, SchemaContext, TypeInfo, Union,
    },
};

impl<S: SchemaContext> Object<S> {
    /// Generate SDL for the object type. An example of object type in SDL format is:
    /// ```graphql
    /// """Person Object Type"""
    /// type Person @key(fields: "id") {
    ///   id: ID!
    ///   name: String!
    ///   age: Int!
    ///   posts: [Post!]!
    /// }
    /// ```
    /// Please note that, if there are no fields in the object type, it will return `None`.
    fn generate_sdl<NSGet: NamespacedGetter<S>>(
        &self,
        namespaced_getter: &NSGet,
    ) -> Option<String> {
        let fields_sdl = generate_fields_sdl(&self.fields, namespaced_getter);
        if fields_sdl.is_empty() {
            None
        } else {
            Some(with_description(
                self.description.as_ref(),
                format!(
                    "type {}{} {}",
                    &self.name,
                    generate_directives_sdl(&self.directives, None),
                    in_curly_braces(fields_sdl)
                ),
            ))
        }
    }
}

impl<S: SchemaContext> InputObject<S> {
    /// Generate SDL for the input object type. An example of input object type in SDL format is:
    /// ```graphql
    /// """Insert Person Input Object Type"""
    /// input InsertPersonInput @deprecated(reason: "Use `PersonInput` instead") {
    ///   name: String!
    ///   age: Int!
    /// }
    /// ```
    /// Please note that, if there are no fields in the input object type, it will return `None`.
    fn generate_sdl<NSGet: NamespacedGetter<S>>(
        &self,
        namespaced_getter: &NSGet,
    ) -> Option<String> {
        let fields_sdl = self
            .fields
            .iter()
            .filter_map(|(field_name, field)| {
                namespaced_getter.get(field).map(|(data, _)| {
                    with_description(
                        data.description.as_ref(),
                        format_field_with_type::<S, NSGet>(
                            field_name,
                            &BTreeMap::new(),
                            &data.field_type,
                            &data.deprecation_status,
                            namespaced_getter,
                        ),
                    )
                })
            })
            .collect::<Vec<String>>();
        if fields_sdl.is_empty() {
            None
        } else {
            Some(with_description(
                self.description.as_ref(),
                format!(
                    "input {}{} {}",
                    &self.name,
                    generate_directives_sdl(&self.directives, None),
                    in_curly_braces(fields_sdl)
                ),
            ))
        }
    }
}

impl Scalar {
    /// Generate SDL for the scalar type. An example of scalar type in SDL format is:
    /// ```graphql
    /// """Custom Scalar Type for Date"""
    /// scalar Date
    /// ```
    fn generate_sdl(&self) -> String {
        with_description(
            self.description.as_ref(),
            format!(
                "scalar {}{}",
                &self.name,
                generate_directives_sdl(&self.directives, None)
            ),
        )
    }
}

impl<S: SchemaContext> Enum<S> {
    /// Generate SDL for the enum type. An example of enum type in SDL format is:
    /// ```graphql
    /// """Custom Enum Type for application status"""
    /// enum ApplicationStatus {
    ///   DRAFT @deprecated(reason: "Use `PENDING` instead")
    ///   PENDING
    ///   APPROVED
    ///   REJECTED
    /// }
    /// ```
    /// Please note that, if there are no values in the enum type, it will return `None`.
    fn generate_sdl<NSGet: NamespacedGetter<S>>(
        &self,
        namespaced_getter: &NSGet,
    ) -> Option<String> {
        let fields_sdl = self
            .values
            .values()
            .filter_map(|enum_val| {
                namespaced_getter.get(enum_val).map(|(data, _)| {
                    with_description(
                        data.description.as_ref(),
                        format!(
                            "{} {}",
                            data.value,
                            generate_directives_sdl(&[], Some(&data.deprecation_status))
                        ),
                    )
                })
            })
            .collect::<Vec<String>>();
        if fields_sdl.is_empty() {
            None
        } else {
            Some(with_description(
                self.description.as_ref(),
                format!(
                    "enum {}{} {}",
                    self.name,
                    generate_directives_sdl(&self.directives, None),
                    in_curly_braces(fields_sdl)
                ),
            ))
        }
    }
}

impl<S: SchemaContext> Union<S> {
    /// Generate SDL for the union type. An example of union type in SDL format is:
    /// ```graphql
    /// """Union Type for search results"""
    /// union SearchResult = Human | Droid | Starship
    /// ```
    /// Please note that, if there are no members in the union type, it will return `None`.
    fn generate_sdl<NSGet: NamespacedGetter<S>>(
        &self,
        namespaced_getter: &NSGet,
    ) -> Option<String> {
        let members_sdl = &self
            .members
            .iter()
            .filter_map(|(union_member, member_value)| {
                if namespaced_getter.get(member_value).is_some() {
                    Some(union_member.to_string())
                } else {
                    None
                }
            })
            .collect::<Vec<String>>();
        if members_sdl.is_empty() {
            None
        } else {
            Some(with_description(
                self.description.as_ref(),
                format!(
                    "union {}{} = {}",
                    self.name,
                    generate_directives_sdl(&self.directives, None),
                    members_sdl.join(" | ")
                ),
            ))
        }
    }
}

impl<S: SchemaContext> Interface<S> {
    /// Generate SDL for the interface type. An example of interface type in SDL format is:
    /// ```graphql
    /// """Interface for Node"""
    /// interface Node {
    ///   id: ID!
    /// }
    /// ```
    /// Please note that, if there are no fields in the interface type, it will return `None`.
    fn generate_sdl<NSGet: NamespacedGetter<S>>(
        &self,
        namespaced_getter: &NSGet,
    ) -> Option<String> {
        let fields_sdl = generate_fields_sdl(&self.fields, namespaced_getter);
        if fields_sdl.is_empty() {
            None
        } else {
            Some(with_description(
                self.description.as_ref(),
                format!(
                    "interface {}{} {}",
                    &self.name,
                    generate_directives_sdl(&self.directives, None),
                    in_curly_braces(fields_sdl)
                ),
            ))
        }
    }
}

impl<S: SchemaContext> TypeInfo<S> {
    fn generate_sdl<NSGet: NamespacedGetter<S>>(
        &self,
        namespaced_getter: &NSGet,
    ) -> Option<String> {
        match self {
            TypeInfo::Scalar(scalar) => Some(scalar.generate_sdl()),
            TypeInfo::Enum(enm) => enm.generate_sdl(namespaced_getter),
            TypeInfo::Object(object) => object.generate_sdl(namespaced_getter),
            TypeInfo::Interface(interface) => interface.generate_sdl(namespaced_getter),
            TypeInfo::Union(union) => union.generate_sdl(namespaced_getter),
            TypeInfo::InputObject(input_object) => input_object.generate_sdl(namespaced_getter),
        }
    }
}

impl<S: SchemaContext> Schema<S> {
    pub fn generate_sdl<NSGet: NamespacedGetter<S>>(&self, namespaced_getter: &NSGet) -> String {
        let schema_sdl = get_schema_sdl(self, namespaced_getter);
        self.types
            .iter()
            .fold(schema_sdl, |mut acc, (type_name, type_info)| {
                // Ignore schema related types
                if !type_name.as_str().starts_with("__")
                    && let Some(type_sdl) = type_info.generate_sdl(namespaced_getter)
                {
                    acc.push_str("\n\n");
                    acc.push_str(&type_sdl);
                }
                acc
            })
    }
}

/// Generate SDL for the schema. An example of schema in SDL format is:
/// ```graphql
/// schema {
///   query: Query
///   mutation: Mutation
///   subscription: Subscription
/// }
/// ```
///
/// Please note that query type will always be there in the schema.
///
/// If there is no mutation type, it will not include mutation in the schema. Also, if there is no
/// mutation field in the mutation type, it will not include mutation in the schema.
///
/// If there is no subscription type, it will not include subscription in the schema.
fn get_schema_sdl<S: SchemaContext, NSGet: NamespacedGetter<S>>(
    schema: &Schema<S>,
    namespaced_getter: &NSGet,
) -> String {
    let query_field = format!("query: {} ", &schema.query_type);
    let mutation_field = schema.mutation_type.as_ref().and_then(|t| {
        schema.types.get(t).and_then(|type_info| match type_info {
            TypeInfo::Object(object) => {
                // If there is only __typename in the mutation fields, ignore the mutation altogether
                if object.fields.iter().all(|(k, v)| {
                    (k.as_str() == "__typename") || (namespaced_getter.get(v).is_none())
                }) {
                    None
                } else {
                    Some(format!("mutation: {t} "))
                }
            }
            _ => None,
        })
    });
    let subscription_field = schema
        .subscription_type
        .as_ref()
        .map(|t| format!("subscription: {t} "));
    format!(
        "schema {}",
        in_curly_braces(
            vec![Some(query_field), mutation_field, subscription_field]
                .into_iter()
                .flatten()
                .collect()
        )
    )
}

/// Generate SDL for description. Descriptions are formatted as block strings (with surrounding
/// triple double-quotes) and must follow <https://spec.graphql.org/October2021/#StringValue>
fn generate_description_sdl(description: Option<&String>) -> String {
    description
        .as_ref()
        // Note newline before enclosing `"""`, otherwise if `d` ends in `"` consumers will get a
        // parse error (ENG-1452). To be totally conformant we also need to strip points outside
        // the SourceCharacter range: https://spec.graphql.org/October2021/#sec-Language.Source-Text
        .map(|d| format!("\"\"\"{d}\n\"\"\""))
        .unwrap_or_default()
}

/// Generate SDL for directives. An example of directives in SDL format is:
/// ```graphql
/// @deprecated(reason: "Use `PENDING` instead")
/// ```
fn generate_directives_sdl(
    directives: &[Directive],
    deprecation_status: Option<&DeprecationStatus>,
) -> String {
    let other_directives = directives
        .iter()
        .map(|d| {
            let args = d
                .arguments
                .iter()
                .map(|(k, v)| format!("{}: {}", k, v.to_json()))
                .collect::<Vec<String>>()
                .join(", ");
            format!(
                "@{}{}",
                d.name,
                if args.is_empty() {
                    String::default()
                } else {
                    format!("({args})")
                }
            )
        })
        .collect::<Vec<String>>()
        .join(" ");
    match deprecation_status {
        Some(DeprecationStatus::Deprecated { reason }) => {
            let reason_arg = reason
                .as_ref()
                .map(|r| format!("(reason: {r})"))
                .unwrap_or_default();
            format!("@deprecated{reason_arg} {other_directives}")
        }
        _ => other_directives,
    }
}

/// Generate SDL for fields. This will not include schema related fields (fields starting with __).
fn generate_fields_sdl<S: SchemaContext, NSGet: NamespacedGetter<S>>(
    fields: &BTreeMap<ast::Name, Namespaced<S, Field<S>>>,
    namespaced_getter: &NSGet,
) -> Vec<String> {
    let mut fields_sdl = Vec::new();
    for (field_name, field) in fields {
        // Ignore schema related fields
        if !field_name.as_str().starts_with("__")
            && let Some((data, _)) = namespaced_getter.get(field)
        {
            fields_sdl.push(with_description(
                data.description.as_ref(),
                format_field_with_type(
                    field_name,
                    &data.arguments,
                    &data.field_type,
                    &data.deprecation_status,
                    namespaced_getter,
                ),
            ));
        }
    }
    fields_sdl
}

/// Format an input field as SDL, including default values if present.
///
/// Syntax:
///
///    <field_name>: <field_type> = <default_value>
///
fn format_input_field_with_type(
    field_name: &ast::Name,
    field_type: &ast::TypeContainer<ast::TypeName>,
    deprecation_status: &DeprecationStatus,
    default_value: Option<&ConstValue>,
) -> String {
    let field_sdl = match default_value {
        None => format!("{field_name}: {field_type}"),
        Some(default_value) => {
            let default_value_sdl = default_value.to_json().to_string();
            let mut default_value_lines = default_value_sdl.lines();
            let multiple_lines = {
                default_value_lines.next();
                default_value_lines.next().is_some()
            };
            if multiple_lines {
                format!(
                    "{}: {}\n  = {}",
                    field_name,
                    field_type,
                    with_indent(default_value_sdl.as_str())
                )
            } else {
                format!("{field_name}: {field_type} = {default_value_sdl}")
            }
        }
    };
    if deprecation_status == &DeprecationStatus::NotDeprecated {
        field_sdl
    } else {
        format!(
            "{} {}",
            field_sdl,
            generate_directives_sdl(&[], Some(deprecation_status))
        )
    }
}

/// Format an (output) field as SDL, including field arguments if present.
///
/// Syntax:
///
///    <field_name>(<generate_arguments_sdl(field_arguments)>): <field_type>
///
fn format_field_with_type<S: SchemaContext, NSGet: NamespacedGetter<S>>(
    field_name: &ast::Name,
    field_arguments: &BTreeMap<ast::Name, Namespaced<S, InputField<S>>>,
    field_type: &ast::TypeContainer<ast::TypeName>,
    deprecation_status: &DeprecationStatus,
    namespaced_getter: &NSGet,
) -> String {
    let field_sdl = if field_arguments.is_empty() {
        format!("{field_name}: {field_type}")
    } else {
        let arguments_sdl = generate_arguments_sdl(field_arguments, namespaced_getter);
        let mut arguments_lines = arguments_sdl.lines();
        let multiple_lines = {
            arguments_lines.next();
            arguments_lines.next().is_some()
        };

        if multiple_lines {
            format!(
                "{}(\n{}\n  ): {}",
                field_name,
                with_indent(arguments_sdl.as_str()),
                field_type
            )
        } else {
            format!("{field_name}({arguments_sdl}): {field_type}")
        }
    };
    if deprecation_status == &DeprecationStatus::NotDeprecated {
        field_sdl
    } else {
        format!(
            "{} {}",
            field_sdl,
            generate_directives_sdl(&[], Some(deprecation_status))
        )
    }
}

fn generate_arguments_sdl<S: SchemaContext, NSGet: NamespacedGetter<S>>(
    field_arguments: &BTreeMap<ast::Name, Namespaced<S, InputField<S>>>,
    namespaced_getter: &NSGet,
) -> String {
    field_arguments
        .iter()
        .filter_map(|(field_name, namespaced)| {
            namespaced_getter.get(namespaced).map(|(input_field, _)| {
                format_input_field_with_type(
                    field_name,
                    &input_field.field_type,
                    &input_field.deprecation_status,
                    input_field.default_value.as_ref(),
                )
            })
        })
        .collect::<Vec<String>>()
        .join(",\n")
}

fn with_description(description: Option<&String>, sdl: String) -> String {
    if description.is_some() {
        format!("{}\n{}", generate_description_sdl(description), sdl)
    } else {
        sdl
    }
}

fn in_curly_braces(strings: Vec<String>) -> String {
    format!(
        "{{\n{}\n}}",
        strings
            .into_iter()
            .map(|s| with_indent(&s))
            .collect::<Vec<String>>()
            .join("\n")
    )
}

fn with_indent(sdl: &str) -> String {
    sdl.lines()
        .map(|l| format!("  {l}"))
        .collect::<Vec<String>>()
        .join("\n")
}
